Pressidian
花园入口
笔记
项目
关于
实验室
GitHub
花园入口
笔记
项目
关于
实验室
GitHub

KNOWLEDGE PATHS

笔记库
当前位置
笔记库/前端/三件套/JS/JavaScript

14 模块化

12 分钟阅读 · Note

目录树 578 篇

            • 00 JavaScript学习地图
            • 01 JavaScript基础
            • 02 类型系统与类型转换
            • 03 Number与String
            • 04 Array数组
            • 05 Object对象
            • 06 集合与迭代
            • 07 函数、作用域与this
            • 08 原型、构造函数与类
            • 09 Generator与异步迭代
            • 10 异步编程
            • 11 浏览器与DOM
            • 12 二进制与文件
            • 13 标准内建对象
            • 14 模块化
            • 15 错误处理
            • 16 JSDoc
            • 90 JavaScript知识点总目录
            • 91 JavaScript修正与补充记录
          • 前端模块化
      • 前端技术栈
    • 笔记目录
    • CLAUDE.md
    • Vue 组件与 Render 函数

关联笔记 6

↗00 JavaScript学习地图反向链接↗90 JavaScript知识点总目录反向链接↗91 JavaScript修正与补充记录反向链接↗01 JavaScript基础同一路径↗02 类型系统与类型转换同一路径↗03 Number与String同一路径
  • 14 模块化

14 模块化

> Last Format Time:8/11/2026 21:45:41

本笔记由两个 JavaScript 教程目录中的同主题内容合并而成;全部旧知识保留在“旧笔记知识全集(按来源保留)”中,并由导航逐项索引。


当前前端开发关键要点

> [!important] 学习优先级:P0 > - 浏览器与现代前端构建默认优先 ESM,理解静态分析、tree shaking 和动态 import()。 > - 明确 ESM 的实时绑定与 CommonJS 导出对象语义,处理好默认导出、具名导出和互操作。 > - 避免隐蔽循环依赖;用清晰分层、依赖方向与按需拆包控制模块边界。


知识点导航

[现代JS教程与阮一峰ES6 · 7模块/CJS和ESM](./14 模块化.md)

  • 核心区别对比表
  • 详细解释
  • 来源与历史
  • 语法
  • 加载方式:静态 vs. 动态
  • 加载时机:同步 vs. 异步
  • 值引用 vs. 值拷贝
  • 运行环境
  • 总结与如何选择

[现代JS教程与阮一峰ES6 · 7模块/Module(模块)语法](./14 模块化.md)

  • 导入导出类型不匹配错误
  • 路径错误
  • 重复导入
  • 导出变量未定义
  • 循环依赖
  • 导入导出的语法错误

现代前端补充与纠错

> [!info] 修改标记 > - 【修正】:旧教程中错误、过时或容易误导的内容。 > - 【补充】:旧教程未覆盖、但当前前端开发需要掌握的内容。 > - 【修正代码】:替换或校正了旧代码示例。

> [!warning] 下方 CommonJS/ESM 对比含历史化简。现代 Node.js 已能在满足条件时由 require() 加载同步 ESM,不能再笼统写成“完全不能加载”。

ESM 核心语义

  • 【补充】 ESM 的导入是只读实时绑定,静态结构支持依赖分析、Tree Shaking 和循环依赖检查;模块只求值一次。
  • 【补充】 import() 返回 Promise,适合按路由/功能拆包;import.meta 提供模块环境信息。
  • 【补充】 浏览器相对导入通常需要完整路径;Node/打包器解析还受 package.json 的 type、exports、imports 和条件导出影响。
  • 【补充】 Tree Shaking 依赖静态 ESM 与副作用分析;sideEffects 配置错误可能删掉必要代码或保留无用代码。

CommonJS 边界

  • 【修正】 require() 返回缓存的 module.exports 值;若它是对象,消费者拿到的是该对象引用,不能概括为“值拷贝”。解构赋值才会把当时属性值取到局部绑定。
  • 【修正】 当前 Node.js 可用 require() 加载满足同步条件、且不含顶层 await 的 ESM;具体限制随 Node 版本变化,工程文档应标明运行时版本。
  • 【补充】 CJS 与 ESM 互操作的默认导入、命名导入和循环依赖容易产生差异,库发布应通过真实 Node/打包器矩阵验证。

旧笔记知识全集(按来源保留)

现代JS教程与阮一峰ES6 · 7模块/CJS和ESM

好的,ESM(ECMAScript Modules)和 CJS(CommonJS)是 JavaScript 中两种最主要的模块系统。它们在设计哲学、语法和运行方式上有根本性的区别。

以下是它们之间的核心区别,我用一个对比表格来清晰展示,然后进行详细解释。

核心区别对比表
特性ESM (ECMAScript Modules)CJS (CommonJS)
来源与标准语言标准 (ES6 / ES2015)社区规范 (主要用于 Node.js 环境)
语法 - 导出export default myComponent; <br> export const foo = 'bar';module.exports = myComponent; <br> exports.foo = 'bar';
语法 - 导入import myComponent from './module.js'; <br> import { foo } from './module.js';const myComponent = require('./module');
加载方式静态 (编译时)动态 (运行时)
加载时机静态链接;浏览器可异步获取,top-level await 会使求值异步require() 通常同步加载与求值
运行环境现代浏览器、现代 Node.jsNode.js (传统)、打包工具 (如 Webpack)
Tree Shaking静态结构便于打包器分析动态 require() 较难静态分析
导入语义导入的是只读实时绑定返回缓存的 module.exports 值;对象通常共享引用
文件后缀通常为 .js, .mjs通常为 .js, .cjs
顶层的 thisundefined指向 module.exports
详细解释
来源与历史
  • CJS (CommonJS):诞生于2009年,旨在为浏览器之外的 JavaScript(主要是 Node.js)提供模块系统。它不是一个语言标准,而是一个社区规范。Node.js 的早期版本完全基于 CJS。
  • ESM (ECMAScript Modules):是 JavaScript 语言在 ES2015 (ES6) 标准中正式引入的模块系统。它的目标是为 JavaScript 提供一个统一的、官方的模块方案,无论是在浏览器还是其他环境中。
语法

这是最直观的区别。

  • CJS 使用 require() 和 module.exports/exports:
    // 导出
    module.exports = {
      functionA,
      variableB
    };
    // 或者
    exports.functionA = functionA;

    // 导入
    const myModule = require('./my-module');
    const { functionA } = require('./my-module');
  • ESM 使用 import 和 export:
    // 导出
    export default functionA;
    export const variableB = 'Hello';

    // 导入
    import myModule from './my-module.js';
    import { variableB } from './my-module.js';
    // 或者混合导入
    import myDefault, { namedExport } from './my-module.js';
加载方式:静态 vs. 动态

这是最关键的区别,它导致了其他所有差异。

  • ESM 是静态的:

    • import 命令必须在模块的顶层作用域使用,不能写在条件判断或函数内。
    • 这意味着模块的依赖关系在代码编译阶段(编译时) 就已经确定下来了。
    • 优势:这使得打包工具(如 Webpack、Rollup)可以轻松地进行静态分析,从而实现 Tree Shaking(摇树优化),剔除那些从未被导出和导入的代码,极大减小最终打包体积。
  • CJS 是动态的:

    • require() 是一个函数,可以在代码的任何地方被调用,包括条件语句和函数内部。
    • 这意味着模块的依赖关系要到代码执行阶段(运行时) 才能确定。
    • 劣势:无法在打包前静态分析出所有依赖,因此难以实现完美的 Tree Shaking。
加载时机:同步 vs. 异步
  • CJS 是同步加载的:require() 语句会同步地阻塞代码执行,直到模块被加载并执行完毕,才会继续执行后面的代码。这在服务器端(Node.js)读写本地文件是可行的。 8/11/26 【修正】原内容:ESM 一概属于异步加载;修改点:静态链接、宿主获取和模块求值是不同阶段,浏览器获取模块通常异步,top-level await 会让依赖图求值异步。
  • ESM 先静态链接,再由宿主加载和求值:浏览器会获取模块依赖图且不按经典脚本方式阻塞 HTML 解析;是否发生异步求值还取决于 top-level await。
值引用 vs. 值拷贝

这个区别在处理循环依赖时尤为明显。

  • ESM 是值的动态只读引用:
    • 导入绑定会实时读取导出模块中的绑定,不应解释成两个模块指向同一个内存地址。
    • 当导出的模块修改了一个值,导入模块中看到的值也会随之改变。
    • 导入的值是只读的,你不能直接修改它(除非导出的是一个对象,你可以修改其属性)。

8/11/26 【修正】原内容:CJS 导入统一属于值拷贝;修改点:require() 返回缓存的 module.exports 值,对象导出通常是共享引用,解构才取得当时的属性值。

  • CJS 的导入语义取决于导出值和消费方式:
    • require() 返回缓存的 module.exports 值;若导出对象,消费者通常共享同一个对象引用。
    • const { value } = require(...) 会在解构时取得属性值;后续重新赋值导出属性不会自动更新这个局部变量。

示例

// counter.js (ESM)
export let count = 0;
export const increment = () => { count++; };

// main.js (ESM)
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1 (值被改变了,因为是对同一内存地址的引用)

// counter.js (CJS)
let count = 0;
exports.increment = () => { count++; };
exports.getCount = () => count; // 需要一个getter来获取最新值

// main.js (CJS)
const { increment, getCount } = require('./counter');
let count = getCount();
console.log(count); // 0
increment();
console.log(getCount()); // 1
console.log(count); // 0 (原始的count变量仍是拷贝时的值,未变)
运行环境
  • CJS:原生支持于 Node.js。在浏览器中直接使用需要打包工具处理。
  • ESM:
    • 现代浏览器 已原生支持 (&lt;script type="module"&gt;)。
    • 现代 Node.js (v12+) 也已原生支持,通常通过 .mjs 文件扩展名或 package.json 中设置 "type": "module" 来启用。
总结与如何选择
  • 新项目:毫无疑问应该使用 ESM。它是语言标准,支持 Tree Shaking,并且是未来的方向。
  • Node.js 开发:
    • 新项目建议在 package.json 中设置 "type": "module" 来使用 ESM。
    • 旧的库和项目可能仍然使用 CJS。
  • 旧项目/库:如果是一个旧的 CJS 项目,除非有充分理由,否则迁移到 ESM 的成本可能较高。但如果要发布新的库,最好提供 ESM 格式的版本,以便用户能进行 Tree Shaking。
  • 在 Node.js 中互操作:
    • 在 ESM 模块中可以使用 import() 动态导入 CJS 模块。 8/11/26 【修正】原内容:CJS 无法用 require() 加载 ESM;修改点:当前 Node.js 可加载满足同步条件且不含顶层 await 的 ESM,限制应按运行时版本确认。
  • CJS/ESM 互操作应按 Node.js 目标版本、模块同步性和包导出配置验证。

简单来说,ESM 是现代化开发的标配,而 CJS 是历史遗留和特定环境(如传统 Node.js)下的产物。

现代JS教程与阮一峰ES6 · 7模块/Module(模块)语法

在ES6模块中,导入和导出的使用如果不规范,很容易产生错误。以下是一些常见错误及其解决方法:

导入导出类型不匹配错误

错误表现:如前面提到的,使用默认导出的模块却用具名导入方式,或者使用具名导出的模块却用默认导入方式,导致找不到对应导出项,报错 No matching export in "xxx" for import "xxx" 。 错误示例:

  • 导出文件(module.js):
const data = { message: 'Hello' };
export default data;
  • 导入文件(main.js):
import { data } from './module.js'; // 错误,这里使用了具名导入,但module.js是默认导出

解决方法:

  • 如果是默认导出,导入时直接使用默认导入形式:
import data from './module.js'; // 正确,使用默认导入
  • 如果想使用具名导入,将导出文件修改为具名导出:
export const data = { message: 'Hello' };

然后在导入文件中使用具名导入:

import { data } from './module.js'; // 正确,使用具名导入
路径错误

错误表现:导入模块时指定了错误的文件路径,导致无法找到模块,报错类似于 Cannot find module 'xxx' 。 错误示例:

import { someFunction } from '../wrongPath/module.js'; // 假设模块实际在../correctPath/module.js

解决方法:仔细检查并确保导入路径与模块实际存放路径一致,修改为正确路径:

import { someFunction } from '../correctPath/module.js';
重复导入

错误表现:多次导入同一个模块,虽然在ES6中重复导入不会报错,但可能会导致代码可读性变差,且存在不必要的资源消耗。 错误示例:

import { someFunction } from './module.js';
import { anotherFunction } from './module.js';
// 又一次导入整个模块
import * as module from './module.js';

解决方法:尽量将多次导入合并为一次导入,按需获取需要的导出项:

import { someFunction, anotherFunction } from './module.js';

或者使用通配符导入后按需访问:

import * as module from './module.js';
const result1 = module.someFunction();
const result2 = module.anotherFunction();
导出变量未定义

错误表现:在导出时,导出了一个未定义的变量,导致运行时报错。 错误示例:

export { nonExistentVariable }; // nonExistentVariable未定义

解决方法:确保导出的变量已经被正确声明和赋值:

const definedVariable = 'This is defined';
export { definedVariable };
循环依赖

错误表现:模块A导入模块B,而模块B又导入模块A,形成循环引用,可能导致某些变量未初始化就被使用,从而报错。 错误示例:

  • moduleA.js:
import { bFunction } from './moduleB.js';
export const aFunction = () => {
    console.log('Inside aFunction');
    bFunction();
};
  • moduleB.js:
import { aFunction } from './moduleA.js';
export const bFunction = () => {
    console.log('Inside bFunction');
    aFunction();
};

解决方法:

  • 重构代码,避免循环依赖,比如将两个模块中互相依赖的部分提取到一个独立的新模块中。
  • 如果无法避免循环依赖,可以尽量确保在循环引用中,被使用的变量在引用前已经被正确初始化。例如,在Node.js环境中,ES6模块的加载机制会先扫描所有导入导出语句,然后按照一定顺序执行模块代码,合理安排代码执行顺序有时能避免问题。
导入导出的语法错误

错误表现:在使用导入导出语法时,写错关键字或者语法格式,比如忘记写 export 或者 import 等,导致语法错误。 错误示例:

const myFunction = () => {
    console.log('Function');
};
function { // 这里少了export关键字,不是一个正确的导出语句
    myFunction();
}

解决方法:仔细检查导入导出的语法,确保关键字拼写正确,语句格式符合ES6规范。比如上面的代码修改为:

const myFunction = () => {
    console.log('Function');
};
export function myExportedFunction() {
    myFunction();
}

通过export命令显式指定输出的代码,再通过import命令输入。

// ES6模块
import { stat, exists, readFile } from 'fs';

ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";。 一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。

// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

或者export {firstName,lastName,year};

通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

上面代码使用as关键字,重命名了函数v1和v2的对外接口。重命名后,v2可以用不同的名字输出两次。

需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

// 报错
export 1;

// 报错
var m = 1;
export m;

上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出 1,第二种写法通过变量m,还是直接输出 1。1只是一个值,不是接口。正确的写法是下面这样。

// 写法一
export var m = 1;

// 写法二
var m = 1;
export {m};

// 写法三
var n = 1;
export {n as m};

上面三种写法都是正确的,规定了对外的接口m。其他脚本可以通过这个接口,取到值1。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。

同样的,function和class的输出,也必须遵守这样的写法。

// 报错
function f() {}
export f;

// 正确
export function f() {};

// 正确
function f() {}
export {f};

目前,export 命令能够对外输出的就是三种接口:函数(Functions),类(Classes),var、let、const 声明的变量(Variables)。

另外,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。 export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。

如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

import { lastName as surname } from './profile.js';

import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。

import {a} from './xxx.js'

a = {}; // Syntax Error : 'a' is read-only;

import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径。如果不带有路径,只是一个模块名,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。

import { myMethod } from 'util';

上面代码中,util是模块文件名,由于不带有路径,必须通过配置,告诉引擎怎么取到这个模块。

注意,import命令具有提升效果,会提升到整个模块的头部,首先执行。

为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

// export-default.js
export default function () {
  console.log('foo');
}

上面代码是一个模块文件export-default.js,它的默认输出是一个函数。

其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

// import-default.js
import customName from './export-default';
customName(); // 'foo'

上面代码的import命令,可以用任意名称指向export-default.js输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时import命令后面,不使用大括号。

如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。

export { foo, bar } from 'my_module';

// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };